Skip to content

fix(oasis): implement prompt-level tool calling in the CLI bridge (agents could never act) - #29

Open
jcartagenamartinez-legaltech wants to merge 2 commits into
amadad:mainfrom
jcartagenamartinez-legaltech:fix/oasis-cli-tool-calling
Open

fix(oasis): implement prompt-level tool calling in the CLI bridge (agents could never act)#29
jcartagenamartinez-legaltech wants to merge 2 commits into
amadad:mainfrom
jcartagenamartinez-legaltech:fix/oasis-cli-tool-calling

Conversation

@jcartagenamartinez-legaltech

Copy link
Copy Markdown

The bug

The CLI model bridge drops tool schemas entirely (CLIModel ignores tool schemas) and always returns plain-text completions with finish_reason=stop. But OASIS agents act on the platform exclusively through tool callsperform_action_by_llm consumes response.info['tool_calls'] — so with CLI providers (claude-cli / codex-cli) agents can never execute a single platform action.

Observable symptoms (every run we executed, 119-185 agents): only 2-4 active rounds out of 15, with the recorded actions coming from env-scripted seeds rather than agent decisions. When we interviewed agents post-run they reported it verbatim: "the social platform tools were not loaded — no like, no comment".

The fix

CLI providers can't do native function calling, so it is implemented at the prompt level, entirely inside oasis_llm.py:

  1. Tool schemas (name, description, JSON parameters) are serialized into an instruction appended to the conversation, requesting a single JSON object: {"tool_calls":[{"name", "arguments"}]} to act, or {"content": "..."} for a plain reply.
  2. The text response is parsed back — tolerating markdown fences, JSON embedded in prose, stringified arguments, and OpenAI-style nested {"function": {...}} — and rebuilt as an OpenAI-shaped ChatCompletion with proper message.tool_calls (id, type=function, function.name, function.arguments as a JSON string) and finish_reason="tool_calls": exactly the contract CAMEL's ChatAgent reads (chat_agent.py builds ToolCallRequest from those three fields).
  3. Tool-protocol messages are normalized on the way in (role=tool results and assistant tool-call echoes become plain text) so multi-turn tool loops survive a text-only provider.
  4. Graceful degradation: responses with no parseable tool call fall back to plain content — interview prompts that explicitly forbid tool use keep working unchanged.

Testing

  • 7-case parser suite: plain JSON, fenced, prose-embedded, string arguments, nested OpenAI format, content fallback, garbage-text fallback.
  • Completion-builder assertions against the exact fields CAMEL consumes (function.name, json.loads(function.arguments), id, finish_reason).
  • Message normalization for tool-role and tool-call-echo messages.
  • Full end-to-end simulation run scheduled on our side (170+ agents, 15 rounds) — happy to report round-activity numbers in a comment once it completes.

Relationship to other PRs

Independent of #26/#27/#28 in diff terms (only touches app/utils/oasis_llm.py). On Windows, CLI providers also need #26 to run at all.

🤖 Generated with Claude Code

The CLI model bridge dropped tool schemas entirely
("CLIModel ignores tool schemas") and always returned plain-text
completions with finish_reason=stop. But OASIS agents act on the
platform exclusively through tool calls (perform_action_by_llm
consumes response.info['tool_calls']), so with CLI providers agents
could never execute a single platform action: simulation rounds
stayed silently empty (observed: 2-4 active rounds out of 15 across
every run), and the few recorded actions came from env-scripted
seeds. Interviewed agents even reported it verbatim: "the social
platform tools were not loaded".

Since CLI providers cannot do native function calling, implement it
at the prompt level:

- Serialize the tool schemas (name, description, JSON parameters)
  into an instruction appended to the conversation, asking for a
  single JSON object: {"tool_calls":[{name, arguments}]} to act, or
  {"content": "..."} for a plain reply.
- Parse the CLI's text response (tolerating markdown fences, JSON
  embedded in prose, stringified arguments and OpenAI-style nested
  {"function": {...}} entries) and rebuild an OpenAI-shaped
  ChatCompletion with proper message.tool_calls entries
  (id, type=function, function.name, function.arguments as a JSON
  string) and finish_reason="tool_calls" — exactly the contract
  CAMEL's ChatAgent consumes to execute FunctionTools.
- Normalize tool-protocol messages on the way in (role=tool results
  and assistant tool_call echoes become plain text) so multi-turn
  tool loops survive the round-trip through a text-only provider.
- Graceful degradation: a response with no parseable tool call
  falls back to plain content, preserving old behavior for
  interview prompts that explicitly forbid tool use.

Tested: 7-case parser suite (fences, prose-embedded JSON, string
arguments, nested format, content fallback), completion-builder
assertions against the exact fields CAMEL reads
(function.name / json.loads(function.arguments) / id), and message
normalization for tool-role and tool-call-echo messages.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Replace the greedy {.*} regex fallback with a raw_decode scan from
  each '{': finds the first VALID JSON object even when the model
  wraps it in prose containing braces (the greedy match spanned from
  first to last brace and lost valid tool calls).
- Filter parsed tool names against the offered tool list: CAMEL
  indexes self._internal_tools[name] without a guard, so a
  hallucinated name would raise KeyError and lose the agent's whole
  round. Unknown names are dropped with a warning instead.
- Trigger the tool-call echo summary on the PRESENCE of tool_calls
  rather than content is None: CAMEL serializes assistant tool-call
  messages with content='' (never None), so the branch was dead and
  multi-turn context was silently lost.
- When the model attempts the protocol but yields neither valid
  calls nor string content, return empty content instead of leaking
  the raw JSON blob as agent-visible text.

Re-ran the parser/builder/normalizer test suite against the updated
module: 7/7 parser cases plus the CAMEL-contract assertions pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@jcartagenamartinez-legaltech

Copy link
Copy Markdown
Author

Empirical validation — before/after on the same real scenario

We re-ran the exact market-simulation scenario (189 personas, Twitter + Reddit, 16 rounds, claude-cli provider on Windows) that originally showed the empty-rounds symptom this PR fixes. Only variable changed: this branch.

Before (main) After (this PR)
Total actions 87 2,035 (~23×)
Rounds with activity 3 of 15 16 of 16
Distinct active agents 21 165 of 189
Action types executed 1 (CREATE_POST only) 10 (full repertoire)
Engagement trend dies after round 3 compounds — peak 352 actions in round 12

Action histogram after the fix: LIKE_COMMENT 661 · LIKE_POST 560 · CREATE_COMMENT 370 · QUOTE_POST 169 · FOLLOW 144 · CREATE_POST 118 · REPOST 6 · DO_NOTHING 5 · dislikes 2. Before the fix, agents could literally only post — every other tool call was silently dropped by the CLI bridge, which is why long simulations looked like "attention decay".

We also ran the stratified interview path from #28 on top of this branch: 50/50 interview transcripts completed, zero empty responses.

Happy to share redacted logs if useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant